/* * JBoss, Home of Professional Open Source * Copyright 2012, 2013, Red Hat Middleware LLC, and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.arquillian.container.was.wlp_managed_8_5; import static java.util.logging.Level.FINER; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.ObjectInstance; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.jboss.arquillian.container.spi.client.container.DeployableContainer; import org.jboss.arquillian.container.spi.client.container.DeploymentException; import org.jboss.arquillian.container.spi.client.container.LifecycleException; import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; import org.jboss.arquillian.container.spi.client.protocol.metadata.HTTPContext; import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; import org.jboss.arquillian.container.spi.client.protocol.metadata.Servlet; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.exporter.ZipExporter; import org.jboss.shrinkwrap.descriptor.api.Descriptor; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; /** * WLPManagedContainer * * @author <a href="mailto:gerhard.poul@gmail.com">Gerhard Poul</a> * @version $Revision: $ */ public class WLPManagedContainer implements DeployableContainer<WLPManagedContainerConfiguration> { private static final String className = WLPManagedContainer.class.getName(); private static Logger log = Logger.getLogger(className); private WLPManagedContainerConfiguration containerConfiguration; private JMXConnector jmxConnector; private MBeanServerConnection mbsc; private Process wlpProcess; private Thread shutdownThread; public void setup(WLPManagedContainerConfiguration configuration) { if (log.isLoggable(Level.FINER)) { log.entering(className, "setup"); } this.containerConfiguration = configuration; if (log.isLoggable(Level.FINER)) { log.exiting(className, "setup"); } } // This method includes parts heavily based on the ManagedDeployableContainer.java in the jboss-as // managed container implementation as written by Thomas.Diesler@jboss.com public void start() throws LifecycleException { if (log.isLoggable(Level.FINER)) { log.entering(className, "start"); } // Find WebSphere Liberty Profile VMs by looking for ws-launch.jar and the name of the server String vmid; VirtualMachine wlpvm = null; String serviceURL = null; try { vmid = findVirtualMachineIdByName(containerConfiguration.getServerName()); // If it has already been started, throw exception unless we explicitly allow connecting to a running server if (vmid != null) { if (!containerConfiguration.isAllowConnectingToRunningServer()) throw new LifecycleException("Connecting to an already running server is not allowed"); wlpvm = VirtualMachine.attach(vmid); serviceURL = getVMLocalConnectorAddress(wlpvm); if (serviceURL == null) throw new LifecycleException("Unable to retrieve connector address for localConnector"); } else { if (containerConfiguration.isAddLocalConnector()) { // Get path to server.xml String serverXML = getServerXML(); if ("defaultServer".equals(containerConfiguration.getServerName()) && !new File(serverXML).exists()) { // If server.xml doesn't exist for the default server, we may be dealing with a new // installation where the server will be created at first // startup. Get the default template server.xml instead. The server.xml for "defaultServer" // will be created from this. serverXML = getDefaultServerXML(); } // Read server.xml file into Memory Document document = readServerXML(serverXML); addFeatures(document, "localConnector-1.0"); writeServerXML(document, serverXML); } // Start the WebSphere Liberty Profile VM List<String> cmd = new ArrayList<String>(); String javaVmArguments = containerConfiguration.getJavaVmArguments(); cmd.add(System.getProperty("java.home") + "/bin/java"); cmd.add("-Dcom.ibm.ws.logging.console.log.level=INFO"); if (!javaVmArguments.equals("")) cmd.add(javaVmArguments); cmd.add("-javaagent:lib/bootstrap-agent.jar"); cmd.add("-jar"); cmd.add("lib/ws-launch.jar"); cmd.add(containerConfiguration.getServerName()); log.finer("Starting server with command: " + cmd.toString()); ProcessBuilder pb = new ProcessBuilder(cmd); pb.directory(new File(containerConfiguration.getWlpHome())); pb.redirectErrorStream(true); wlpProcess = pb.start(); new Thread(new ConsoleConsumer()).start(); final Process proc = wlpProcess; shutdownThread = new Thread(new Runnable() { @Override public void run() { if (proc != null) { proc.destroy(); try { proc.waitFor(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }); Runtime.getRuntime().addShutdownHook(shutdownThread); // Wait up to 30s for the server to start int startupTimeout = containerConfiguration.getServerStartTimeout() * 1000; while (startupTimeout > 0 && serviceURL == null) { startupTimeout -= 500; Thread.sleep(500); // Verify that the process we're looking for is actually running int ev = Integer.MIN_VALUE; // exit value of the process IllegalThreadStateException itse = null; // Will be thrown when process is still running try { ev = wlpProcess.exitValue(); } catch (IllegalThreadStateException e) { itse = e; } if (itse == null) throw new LifecycleException("Process terminated prematurely; ev = " + ev); if (vmid == null) // Find WebSphere Liberty Profile VMs by looking for ws-launch.jar and the name of the server vmid = findVirtualMachineIdByName(containerConfiguration.getServerName()); if (wlpvm == null && vmid != null) wlpvm = VirtualMachine.attach(vmid); if (serviceURL == null && wlpvm != null) serviceURL = getVMLocalConnectorAddress(wlpvm); } // If serviceURL is still null, we were unable to start the virtual machine if (serviceURL == null) throw new LifecycleException("Unable to retrieve connector address for localConnector of started VM"); log.finer("vmid: " + vmid); } } catch (Exception e) { throw new LifecycleException("Could not start container", e); } try { JMXServiceURL url = new JMXServiceURL(serviceURL); jmxConnector = JMXConnectorFactory.connect(url); mbsc = jmxConnector.getMBeanServerConnection(); } catch (IOException e) { throw new LifecycleException("Connecting to the JMX MBean Server failed", e); } if (log.isLoggable(Level.FINER)) { log.exiting(className, "start"); } } private String getVMLocalConnectorAddress(VirtualMachine wlpvm) throws IOException { String serviceURL; String PROPERTY_NAME = "com.sun.management.jmxremote.localConnectorAddress"; serviceURL = wlpvm.getAgentProperties().getProperty(PROPERTY_NAME); // On some environments like the IBM JVM the localConnectorAddress is not // in the AgentProperties but in the SystemProperties. if (serviceURL == null) serviceURL = wlpvm.getSystemProperties().getProperty(PROPERTY_NAME); if (log.isLoggable(Level.FINER)) { log.finer("service url: " + serviceURL); } return serviceURL; } private String findVirtualMachineIdByName(String serverName) { if (log.isLoggable(Level.FINER)) { log.entering(className, "findVirtualMachineIdByName"); } List<VirtualMachineDescriptor> vmds = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : vmds) { String displayName = vmd.displayName(); if (log.isLoggable(Level.FINER)) { log.finer("VMD displayName: " + displayName); log.finer("VMD id: " + vmd.id()); } if (displayName.contains(serverName) && (displayName.contains("ws-server.jar") || displayName.contains("ws-launch.jar"))) { // If VM's display name matches, return. if (log.isLoggable(Level.FINER)) { log.exiting(className, "findVirtualMachineIdByName", vmd.id()); } return vmd.id(); } } // Only reached when VM is not found if (log.isLoggable(Level.FINER)) { log.exiting(className, "findVirtualMachineIdByName"); } return null; } public ProtocolMetaData deploy(final Archive<?> archive) throws DeploymentException { if (log.isLoggable(Level.FINER)) { log.entering(className, "deploy"); log.finer("Archive provided to deploy method: " + archive.toString(true)); } String archiveName = archive.getName(); String archiveType = createDeploymentType(archiveName); String deployName = createDeploymentName(archiveName); try { // If the deployment is to server.xml, then update server.xml with the application information if (containerConfiguration.isDeployTypeXML()) { // Throw error if deployment type is not ear, war, or eba if (!archiveType.equalsIgnoreCase("ear") && !archiveType.equalsIgnoreCase("war") && !archiveType.equalsIgnoreCase("eba")) throw new DeploymentException("Invalid archive type: " + archiveType + ". Valid archive types are ear, war, and eba."); // Save the archive to disk so it can be loaded by the container. String appDir = getAppDirectory(); File exportedArchiveLocation = new File(appDir, archiveName); archive.as(ZipExporter.class).exportTo(exportedArchiveLocation, true); // Read server.xml file into Memory Document document = readServerXML(); // Add the archive as appropriate to the server.xml file addApplication(document, deployName, archiveName, archiveType); // Update server.xml on file system writeServerXML(document); } // Otherwise put the application in the dropins directory else { // Save the archive to disk so it can be loaded by the container. String dropInDir = getDropInDirectory(); File exportedArchiveLocation = new File(dropInDir, archiveName); archive.as(ZipExporter.class).exportTo(exportedArchiveLocation, true); } // Wait until the application is deployed and available waitForApplicationTargetState(deployName, true, containerConfiguration.getAppDeployTimeout()); // Return metadata on how to contact the deployed application ProtocolMetaData metaData = new ProtocolMetaData(); HTTPContext httpContext = new HTTPContext("localhost", getHttpPort()); httpContext.add(new Servlet("ArquillianServletRunner", deployName)); metaData.addContext(httpContext); if (log.isLoggable(Level.FINER)) { log.exiting(className, "deploy"); } return metaData; } catch (Exception e) { throw new DeploymentException("Exception while deploying application.", e); } } private int getHttpPort() throws DeploymentException { if (log.isLoggable(Level.FINER)) { log.entering(className, "getHttpPort"); } int httpPort = containerConfiguration.getHttpPort(); if (httpPort == 0) httpPort = getHttpPortFromChannelFWMBean("defaultHttpEndpoint"); if (log.isLoggable(Level.FINER)) { log.exiting(className, "getHttpPort", httpPort); } return httpPort; } // Returns the HttpPort configured on the Channel Framework MBean with the provided endpoint name private int getHttpPortFromChannelFWMBean(String endpointName) throws DeploymentException { if (log.isLoggable(Level.FINER)) { log.entering(className, "getHttpPortFromChannelFWMBean", endpointName); } ObjectName endpointMBean = null; try { endpointMBean = new ObjectName( "WebSphere:feature=channelfw,type=endpoint,name=" + endpointName); } catch (MalformedObjectNameException e) { throw new DeploymentException( "The generated object name is wrong. The endpointName used was '" + endpointName + "'", e); } catch (NullPointerException e) { // This should never happen given that the name parameter to the // ObjectName constructor above can never be null throw new DeploymentException("This should never happen", e); } int httpPort; try { if (!mbsc.isRegistered(endpointMBean)) throw new DeploymentException("The Channel Framework MBean with endpointName '" + endpointName + "' does not exist."); httpPort = ((Integer)mbsc.getAttribute(endpointMBean, "Port")).intValue(); log.finer("httpPort: " + httpPort); } catch (Exception e) { throw new DeploymentException( "Exception while retrieving httpPort information from Channel Framework MBean. " + "The httpPort can also be manually configured in the arquillian container configuration.", e); } if (log.isLoggable(Level.FINER)) { log.exiting(className, "getHttpPortFromChannelFWMBean", httpPort); } return httpPort; } public void undeploy(final Archive<?> archive) throws DeploymentException { if (log.isLoggable(Level.FINER)) { log.entering(className, "undeploy"); } String archiveName = archive.getName(); String deployName = createDeploymentName(archiveName); try { // If deploy type is xml, then remove the application from the xml file, which causes undeploy if (containerConfiguration.isDeployTypeXML()) { // Read the server.xml file into Memory Document document = readServerXML(); // Remove the archive from the server.xml file removeApplication(document); // Update server.xml on file system writeServerXML(document); // Wait until the application is undeployed waitForApplicationTargetState(deployName, false, containerConfiguration.getAppUndeployTimeout()); // Remove archive from the apps directory String appDir = getAppDirectory(); File exportedArchiveLocation = new File(appDir, archiveName); if (!exportedArchiveLocation.delete()) throw new DeploymentException("Unable to delete archive from apps directory"); } else { // Remove archive from the dropIn directory, which causes undeploy String dropInDir = getDropInDirectory(); File exportedArchiveLocation = new File(dropInDir, archiveName); if (!exportedArchiveLocation.delete()) throw new DeploymentException("Unable to delete archive from dropIn directory"); // Wait until the application is undeployed waitForApplicationTargetState(deployName, false, containerConfiguration.getAppUndeployTimeout()); } } catch (Exception e) { throw new DeploymentException("Exception while undeploying application.", e); } if (log.isLoggable(Level.FINER)) { log.exiting(className, "undeploy"); } } private String getDropInDirectory() { String dropInDir = containerConfiguration.getWlpHome() + "/usr/servers/" + containerConfiguration.getServerName() + "/dropins"; if (log.isLoggable(Level.FINER)) log.finer("dropInDir: " + dropInDir); return dropInDir; } private String getAppDirectory() { String appDir = containerConfiguration.getWlpHome() + "/usr/servers/" + containerConfiguration.getServerName() + "/apps"; if (log.isLoggable(Level.FINER)) log.finer("appDir: " + appDir); return appDir; } private String getServerXML() { String serverXML = containerConfiguration.getWlpHome() + "/usr/servers/" + containerConfiguration.getServerName() + "/server.xml"; if (log.isLoggable(Level.FINER)) log.finer("server.xml: " + serverXML); return serverXML; } // templates/servers/defaultServer/server.xml private String getDefaultServerXML() { String serverXML = containerConfiguration.getWlpHome() + "/templates/servers/defaultServer/server.xml"; if (log.isLoggable(FINER)) { log.finer("default server.xml: " + serverXML); } return serverXML; } private String createDeploymentName(String archiveName) { return archiveName.substring(0, archiveName.lastIndexOf(".")); } private String createDeploymentType(String archiveName) { return archiveName.substring(archiveName.lastIndexOf(".")+1); } private Document readServerXML() throws DeploymentException { return readServerXML(getServerXML()); } private Document readServerXML(String serverXML) throws DeploymentException { try { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); return documentBuilder.parse(new File(serverXML)); } catch (Exception e) { throw new DeploymentException("Exception while reading server.xml file.", e); } } private void writeServerXML(Document doc) throws DeploymentException { writeServerXML(doc, getServerXML()); } private void writeServerXML(Document doc, String serverXML) throws DeploymentException { try { TransformerFactory tf = TransformerFactory.newInstance(); Transformer tr = tf.newTransformer(); tr.setOutputProperty(OutputKeys.INDENT, "yes"); DOMSource source = new DOMSource(doc); StreamResult res = new StreamResult(new File(serverXML)); tr.transform(source, res); } catch (Exception e) { throw new DeploymentException("Exception wile writing server.xml file.", e); } } private Element createFeature(Document doc, String featureName) { Element feature = doc.createElement("feature"); feature.appendChild(doc.createTextNode(featureName)); return feature; } private void addFeatures(Document doc, String featureNames) { NodeList rootList = doc.getElementsByTagName("featureManager"); Node featureManager = rootList.item(0); for (String featureName : featureNames.split(",")) { if (!checkFeatureAlreadyThere(featureName, featureManager.getChildNodes())) { featureManager.appendChild(createFeature(doc, featureName)); } } } private boolean checkFeatureAlreadyThere(String featureName, NodeList featureManagerList) { for (int i=0; i<featureManagerList.getLength(); i++) { Node feature = featureManagerList.item(i); if ("feature".equals(feature.getNodeName())) { Node featureText = feature.getFirstChild(); if (featureText != null && featureText.getTextContent().trim().equals(featureName)) { return true; } } } return false; } private Element createApplication(Document doc, String deploymentName, String archiveName, String type) { // create new Application Element application = doc.createElement("application"); application.setAttribute("id", deploymentName); application.setAttribute("location", archiveName); application.setAttribute("name", deploymentName); application.setAttribute("type", type); // create shared library if (containerConfiguration.getSharedLib() != null) { Element sharedLib = doc.createElement("classloader"); sharedLib.setAttribute("commonLibraryRef", containerConfiguration.getSharedLib()); application.appendChild(sharedLib); } return application; } private void addApplication(Document doc, String deployName, String archiveName, String type) { NodeList rootList = doc.getElementsByTagName("server"); Node root = rootList.item(0); root.appendChild(createApplication(doc, deployName, archiveName, type)); } private void removeApplication(Document doc) { Node server = doc.getElementsByTagName("server").item(0); NodeList serverlist = server.getChildNodes(); for (int i=0; serverlist.getLength() > i; i++) { Node node = serverlist.item(i); if (node.getNodeName().equals("application")) { node.getParentNode().removeChild(node); } } } private void waitForApplicationTargetState(String applicationName, boolean targetState, int timeout) throws DeploymentException { if (log.isLoggable(Level.FINER)) { log.entering(className, "waitForMBeanTargetState"); } ObjectName appMBean = null; ObjectName listAllApps = null; try { appMBean = new ObjectName("WebSphere:service=com.ibm.websphere.application.ApplicationMBean,name=" + applicationName); listAllApps = new ObjectName("WebSphere:service=com.ibm.websphere.application.ApplicationMBean,name=*"); } catch (MalformedObjectNameException e) { throw new DeploymentException("The generated object name is wrong. The applicationName used was '" + applicationName + "'", e); } catch (NullPointerException e) { // This should never happen given that the name parameter to the // ObjectName constructor above can never be null throw new DeploymentException("This should never happen", e); } // Loop until the application MBean has reached the target state or until the timeout try { int timeleft = timeout * 1000; while(mbsc.isRegistered(appMBean) != targetState) { Thread.sleep(100); if (timeleft <= 0) { Set<ObjectInstance> allApps = mbsc.queryMBeans(/*listAllApps*/ null, null); log.fine("Size of results: " + allApps.size()); for (ObjectInstance app : allApps) { log.fine(app.getObjectName().toString()); } throw new DeploymentException("Timeout while waiting for ApplicationMBean to reach targetState"); } timeleft -= 100; } // If the target state is true (true==STARTED) // then loop until the deployed application is in started state or until the timeout if (targetState == true) { String applicationState = null; while(applicationState == null || !applicationState.contentEquals("STARTED")) { Thread.sleep(100); applicationState = (String)mbsc.getAttribute(appMBean, "State"); if (timeleft <= 0) throw new DeploymentException("Timeout while waiting for ApplicationState to reach STARTED"); timeleft -= 100; } } } catch (Exception e) { throw new DeploymentException("Exception while checking application state.", e); } if (log.isLoggable(Level.FINER)) { log.exiting(className, "waitForMBeanTargetState"); } } public void stop() throws LifecycleException { if (log.isLoggable(Level.FINER)) { log.entering(className, "stop"); } try { jmxConnector.close(); } catch (IOException e) { throw new LifecycleException("Communication with the MBean Server failed.", e); } if (shutdownThread != null) { Runtime.getRuntime().removeShutdownHook(shutdownThread); shutdownThread = null; } try { if (wlpProcess != null) { wlpProcess.destroy(); wlpProcess.waitFor(); wlpProcess = null; } } catch (Exception e) { throw new LifecycleException("Could not stop container", e); } if (log.isLoggable(Level.FINER)) { log.exiting(className, "stop"); } } public ProtocolDescription getDefaultProtocol() { if (log.isLoggable(Level.FINER)) { log.entering(className, "getDefaultProtocol"); } String defaultProtocol = "Servlet 3.0"; if (log.isLoggable(Level.FINER)) { log.exiting(className, "getDefaultProtocol", defaultProtocol); } return new ProtocolDescription(defaultProtocol); } @Override public Class<WLPManagedContainerConfiguration> getConfigurationClass() { return WLPManagedContainerConfiguration.class; } public void deploy(Descriptor descriptor) throws DeploymentException { // TODO Auto-generated method stub } public void undeploy(Descriptor descriptor) throws DeploymentException { // TODO Auto-generated method stub } /** * Runnable that consumes the output of the process. If nothing consumes the output the process will hang on some platforms * Implementation from wildfly's ManagedDeployableContainer.java * * @author Stuart Douglas */ private class ConsoleConsumer implements Runnable { @Override public void run() { final InputStream stream = wlpProcess.getInputStream(); final boolean writeOutput = containerConfiguration.isOutputToConsole(); try { byte[] buf = new byte[32]; int num; // Do not try reading a line cos it considers '\r' end of line while ((num = stream.read(buf)) != -1) { if (writeOutput) System.out.write(buf, 0, num); } } catch (IOException e) { } } } }